package com.googlecode.cswish.struts.spring;

import static com.googlecode.cswish.asm.Opcodes.ACC_PUBLIC;
import static com.googlecode.cswish.asm.Opcodes.ACC_SUPER;
import static com.googlecode.cswish.asm.Opcodes.V1_6;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.asm.ClassWriter;
import com.googlecode.cswish.struts.hibernate.DClassLoader;
import com.googlecode.cswish.struts.spring.generator.MapWrapHelper;
import com.googlecode.cswish.struts.spring.generator.MethodWrapHelper;
import com.googlecode.cswish.struts.spring.generator.ValueObjectHelper;
import com.googlecode.cswish.struts.spring.generator.ValueObjectIntfHelper;
import com.googlecode.cswish.util.BlankObject;
import com.googlecode.cswish.util.ClassEx;

/**
 * Create class dynamically.
 * <p> 
 * TODO: Use Groovy engine or Javasist to generate the class? the code is hard to 
 * understand by calling ASM directly.
 * <pre>
 * ScriptEngineManager factory = new ScriptEngineManager();
 * ScriptEngine engine = factory.getEngineByName("groovy");
 * </pre>
 * @author Jerry.Feng Date: 27/08/2008
 *
 */
@Service
public class DFactory {
	
	private static final Logger logger = Logger.getLogger(DFactory.class);
	
	/**
	 * Support annotations, such as @Inject, @ParamScope etc
	 * 
	 * Convert method with parameters to non-parameter instance, Notice: it doesn't
	 * support inner class method.
	 * 
	 *  <p> For example: <br>  
	 *  <pre>
	 *  Method: String display(String value)
	 *  
	 *  ==>
	 *  
	 *  Class A implements Map {
	 *  	// Getter/Setter
	 *  	public String getDisplay();
	 *  	public void setDisplay(String v);
	 *  
	 *  	public String getValue();
	 *  	public void setValue(String value);
	 *  
	 *  	// map method, improve the performance of OGNL
	 *  	public Object get(String name);
	 *  	public Object set(String name, Object value);
	 *  
	 *  	// Action method
	 *      // GeneratorHelper.create(serviceType).display()
	 *  	public String display();
	 *  }
	 *  </pre>
	 * @param serviceType 
	 * 
	 * @param method
	 * @param actionName
	 * @param superClassName
	 * @param newMethodName
	 * @param callOldMethod
	 * @param returnValue
	 * @return
	 */
	public Class<?> convert2Class(Class<?> serviceType, Method method,
			String className, Class<?> superClass,
			String newMethodName, boolean callOldMethod, Object returnValue) {
		Class<?> voClass = null;
		try {
			voClass = Class.forName(className, true, Thread.currentThread().getContextClassLoader());
		} catch (Exception ex) {
		}
		// the class has been there
		if (voClass != null) {
			return voClass;
		}
		if (newMethodName == null) {
			newMethodName = method.getName();
		}
		
		List<NameType> properties = populateProperty(method, newMethodName);
		String classDefName = className.replace('.', '/');
		String superClassDefName;
		
		if (superClass == null) {
			superClassDefName = "java/lang/Object";
		} else {
			superClassDefName = superClass.getName().replace('.', '/');
		}
		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
		cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, 
				classDefName,    					// class name
				null,
				superClassDefName,					// super class name
				new String[] {
//					voIntf.getName().replace('.', '/'), 
					"java/util/Map" 
				}				// interface name
		);
		
		
		ValueObjectHelper.defineValueObject(classDefName, superClassDefName, properties, cw);
		if (superClass != null) {
			// find parent getter/setter method, add it to map support 
			try {
				BeanInfo beanInfo = Introspector.getBeanInfo(superClass);
				
		        PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
				for (PropertyDescriptor descriptor : descriptors) {
					if (descriptor.getReadMethod() != null && descriptor.getWriteMethod() != null) {
						NameType nameType = new NameType();
						properties.add(nameType);
						
						nameType.name = descriptor.getDisplayName();
						nameType.type = descriptor.getPropertyType().getName();

						String[] types = ClassEx.getGenericTypes(descriptor.getReadMethod());
						nameType.genericType = types[types.length - 1];
						nameType.annotations = BlankObject.AnnotationArray;
					}
				}
			} catch (IntrospectionException ex) {
	        	logger.error("Can't get bean information:" + superClass.getName(), ex);
	        }
		}
		MapWrapHelper.defineMap(classDefName, properties, cw);
		MethodWrapHelper.wrap(serviceType, method, classDefName, newMethodName, callOldMethod, returnValue, cw);
		
		cw.visitEnd();
		byte[] bytecode = cw.toByteArray();
		
		Class<?> clazz = null;
		try {
			clazz = defineClass(className, bytecode);
		} catch (Exception ex) {
			try {
				clazz = Class.forName(className, true, Thread.currentThread().getContextClassLoader());
			} catch (ClassNotFoundException e) {
				logger.error("can't create class - " + className, ex);
				e.printStackTrace();
			}
		}
		return clazz;
	}
	
	private Class defineClass(String className, byte[] bytecode) throws Exception {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		Class clazz;
		if (classLoader instanceof DClassLoader) {
			clazz = ((DClassLoader)classLoader).defineClass(className, bytecode);
		} else {
			clazz = ReflectUtils.defineClass(className, bytecode, classLoader);
		}
		return clazz;
	}
	
	public Class<?> convert2Class(Method method) {
		return convert2Class(method.getDeclaringClass(), method, method.getDeclaringClass().getName() + '$' + method.getName(),
				null, null, true, "");
	}
	
	/**
	 * Define an interface - it's a simple value object
	 * 
	 * @param name
	 * @param properties
	 * @return
	 */
	public Class<?> defineVOIntf(String name, NameType[] properties) {
		// @see InterfaceMaker
		Class<?> voIntf = null;
		try {
			voIntf = Class.forName(name);
		} catch (Exception ex) {
		}
		// the class has been there
		if (voIntf != null) {
			return voIntf;
		}
		
		byte[] bytecode = ValueObjectIntfHelper.defineVOIntf(name, properties);
		
		try {
			voIntf = defineClass(name, bytecode);
		} catch (Exception ex) {
			logger.error("can't create class - " + name, ex);
		}
		
		return voIntf;
	}
	
	public Class<?> defineVO(String className, Class superClass, Class[] interfaces, List<NameType> properties) {
		// @see InterfaceMaker
		Class<?> voClass = null;
		try {
			voClass = Thread.currentThread().getContextClassLoader().loadClass(className);
		} catch (Exception ex) {
		}
		// the class has been there
		if (voClass != null) {
			return voClass;
		}
		
		String classDefName = className.replace('.', '/');
		String superClassDefName;
		if (superClass == null) {
			superClassDefName = "java/lang/Object";
		} else {
			superClassDefName = superClass.getName().replace('.', '/');
		}
		
		String[] interfaceNames;
		if (interfaces == null) {
			interfaceNames = null;
		} else {
			interfaceNames = new String[interfaces.length];
			
			for (int i = 0; i < interfaces.length; i++) {
				interfaceNames[i] = interfaces[i].getName().replace('.', '/');
			}
		}
		
		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
		cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, 
				classDefName,    					// class name
				null,
				superClassDefName,					// super class name
				interfaceNames						// interface name
		);
		
		
		ValueObjectHelper.defineValueObject(classDefName, superClassDefName, properties, cw);
		cw.visitEnd();
		byte[] bytecode = cw.toByteArray();
		
		try {
			voClass = defineClass(className, bytecode);
		} catch (Exception ex) {
			logger.error("can't create class - " + className, ex);
		}
		
		return voClass;
	}
	
	/**
	 * Populate the parameter information as class property
	 * 
	 * @param method
	 * @return
	 */
	protected List<NameType> populateProperty(Method method, String newMethodName) {
		String [] paramNames = ClassEx.getProtectedParameterNames(method);
		
		// calculate property length
		int propertyLen = paramNames.length;
		boolean hasReturnValue = false;		
		Class<?> returnType = method.getReturnType();
		if (!returnType.equals(void.class)) {
			propertyLen++;
			hasReturnValue = true;
		}
		
		// initialize parameter
		Class<?>[] types = method.getParameterTypes();
		String[] genericTypes = ClassEx.getGenericTypes(method);
		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
		List<NameType> nameTypes = new ArrayList<NameType>(propertyLen);
		for (int i = 0; i < paramNames.length; i++) {
			NameType nameType = new NameType();
			nameTypes.add(nameType);
			
			nameType.name = paramNames[i];
			nameType.type = types[i].getName();
			nameType.genericType = genericTypes[i];
			nameType.annotations = parameterAnnotations[i];
		}
		
		// add return type
		if (hasReturnValue) {
			NameType nameType = new NameType();
			nameTypes.add(nameType);
			
			nameType.name = newMethodName;
			Class<?> type = method.getReturnType();
			nameType.type = type == null ? null : method.getReturnType().getName();
			nameType.genericType = genericTypes[genericTypes.length - 1];
			nameType.annotations = method.getAnnotations();
			nameType.disableSetter = true;
		}
		return nameTypes;
	}
}